package com.google.wave.extensions.code;

import java.util.HashSet;
import java.util.Set;
import java.util.logging.Logger;

import com.google.wave.api.AbstractRobotServlet;
import com.google.wave.api.Annotation;
import com.google.wave.api.Blip;
import com.google.wave.api.Event;
import com.google.wave.api.EventType;
import com.google.wave.api.Range;
import com.google.wave.api.RobotMessageBundle;
import com.google.wave.api.TextView;
import com.google.wave.extensions.code.eval.CodeEvaluator;
import com.google.wave.extensions.code.eval.CodeEvaluatorFactory;
import com.google.wave.extensions.code.eval.CodeResult;

@SuppressWarnings("serial")
public class CodeServlet extends AbstractRobotServlet {
    private static final Logger logger =
	Logger.getLogger(CodeServlet.class.getName());

    private static final String ERROR_ANNOTATION_NAME = "style/textDecoration";
    private static final String ERROR_ANNOTATION_VALUE = "underline";

    private static final String CODE_ANNOTATION_NAME = "style/fontFamily";
    private static final String CODE_ANNOTATION_VALUE = "courier new,monospace";

    private static final String SELECTION_ANNOTATION_PREFIX = "user/r/";

    private static final String LANG_ANNOTATION_NAME = "code-wave/language";

    @Override
    public void processEvents(RobotMessageBundle bundle) {
	Set<Blip> changedBlips = new HashSet<Blip>();
	for (Event e : bundle.getEvents()) {
	    logger.fine("Got event: " + e.getType() + ": " + e);

	    if (e.getType() == EventType.DOCUMENT_CHANGED) {
		if (shouldSkipEvent(e)) {
		    logger.fine("Skipping event: " + e);
		    continue;
		}

		Blip changedBlip = e.getBlip();
		if (changedBlip.getCreator().contains("code-wave")) {
		    logger.fine("Skipping blip: " + changedBlip.getBlipId());
		    continue;
		}

		changedBlips.add(changedBlip);
	    }
	}

	for (Blip changedBlip : changedBlips) {
	    TextView document = changedBlip.getDocument();

	    for (Annotation langAnnotation : document.getAnnotations(LANG_ANNOTATION_NAME)) {
		Range range = langAnnotation.getRange();
		String text = document.getText(range);

		logger.info(changedBlip.getBlipId() + "@" + range + ": " + text);

		String language = langAnnotation.getValue();
		CodeEvaluator evaluator = CodeEvaluatorFactory.getEvaluator(language);

		// Make sure the code is monospaced so the user knows
		// it was recognized as code.
		//
		// TODO(schwardo): Some syntax highlighting here would be awesome.
		document.setAnnotation(range, CODE_ANNOTATION_NAME, CODE_ANNOTATION_VALUE);

		CodeResult result = evaluator.eval(text);
		StringBuilder resultText = new StringBuilder();

		if (result.isSuccess()) {
		    for (Annotation annotation : WaveUtils.getAnnotationsWithin(document, range)) {
			// Check for any user selection ranges and
			// display partial results.  This provides a
			// (very) rudimentary but multi-user debugger.
			if (annotation.getName().startsWith(SELECTION_ANNOTATION_PREFIX)) {
			    Range partialRange = new Range(range.getStart(), annotation.getRange().getEnd());
			    String partial = document.getText(partialRange);
			    CodeResult partialResult = evaluator.eval(partial);
			    if (partialResult.hasReturnValue()) {
				int lineNumber = convertRangeToLineNumber(document, partialRange);
				resultText.append("... line " + lineNumber);
				resultText.append(":  " + partialResult.getReturnValueString() + "\n");
			    }
			}
		    }
		}

		// Now append the actual results
		resultText.append(renderResult(result));
		// Then append them to a child blip (creating one if necessary).
		createOrReplaceExistingBlip(changedBlip, resultText.toString());

		if (result.isSuccess()) {
		    // If we succeeded, make sure the text isn't underlined.
		    WaveUtils.deleteAnnotations(document, range, ERROR_ANNOTATION_NAME);
                    logger.info("=> " + result.getReturnValueString());
		} else {
		    // If we failed, underline the source of the error.
		    Range errorRange = trimRangeToCompilerError(document, range, result);
		    document.setAnnotation(errorRange, ERROR_ANNOTATION_NAME, ERROR_ANNOTATION_VALUE);
		}
	    }
	}
    }

    /**
     * Ensures that we don't get stuck in an infite loop by skipping
     * robot-generated events.
     */
    private boolean shouldSkipEvent(Event e) {
	if (e.getModifiedBy().contains("appspot.com") || e.getModifiedBy().contains("gwave.com")) {
	    return true;
	}
	return false;
    }

    /**
     * If {@code parentBlip} contains a child created by us, replace
     * its contents with {@code newText}.  Otherwise create a new
     * child node.
     */
    private void createOrReplaceExistingBlip(Blip parentBlip, String newText) {
        for (Blip child : parentBlip.getChildren()) {
	    if (child.getCreator().contains("code-wave")) {
		logger.fine("Found existing child blip: " + child.getBlipId());
		child.getDocument().replace(newText);
		return;
	    }
        }

	Blip newBlip = parentBlip.createChild();
	logger.fine("No child found, created new child: " + newBlip.getBlipId());
	newBlip.getDocument().append(newText);
    }

    private int convertRangeToLineNumber(TextView document, Range range) {
	int lineNumber = 1;
	int lastIndex = -1;
	String text = document.getText(range);
	while ((lastIndex = text.indexOf("\n", lastIndex + 1)) != -1) {
	    // If the last character is a newline, we want to skip that line.
	    if (lastIndex < text.length() - 1) {
		lineNumber++;
	    }
	}
	return lineNumber;
    }

    private Range trimRangeToCompilerError(TextView document, Range fullRange, CodeResult result) {
	int lineNumber = result.getErrorLineNumber();
	int colNumber = result.getErrorColumnNumber();

	if (lineNumber == -1 || colNumber == -1) {
	    return fullRange;
	}

	String text = document.getText(fullRange);
	int pos = 0;
	while (lineNumber > 1) {
	    int newline = text.indexOf("\n", pos);
	    if (newline == -1) {
		return fullRange;
	    }
	    pos = newline + 1;
	}
	return new Range(pos + colNumber - 1, fullRange.getEnd());
    }

    private String renderResult(CodeResult result) {
	if (result.isSuccess()) {
	    if (result.hasReturnValue()) {
		// Unicode 0x21D2 is a => symbol.
		return "\u21D2 " + result.getReturnValueString();
	    } else {
		return "";
	    }
	} else {
	    return result.getErrorMessage();
	}
    }
}
